Polymorphism
In order to build usable, accessible interfaces, it's important that we understand the semantics of different HTML tags.
For example: if an element can be clicked to perform an action in JS, it should be a button! Unless that action is to navigate the user to a new page, in which case, it should be an anchor (<a>).
(If you'd like to learn more about why adding a click-handler to a <div> is such a bad idea, I recommend checking out React Podcast #34, “Just Use A Button”, with superstar developer Jen Luker. The entire episode is worth listening to, but the button-specific part is around 26:30.)
When choosing an HTML tag, it's much more important to focus on the semantics than the aesthetics. You should use a <button> even if you don't want it to look like a button. With CSS, we can strip away all of those built-in button styles. It's much easier to remove a handful of CSS rules than it is to recreate all of the usability benefits built into the <button> tag.
With all of that in mind, let's suppose our designer wants us to build the following UI:
In the top right, there are some actions the user can take:
These look like links, but are they? It depends on whether clicking them changes the URL or not. “Export All Data” doesn't sound like a link to me; I imagine it generating a .csv and emailing it to the user.
So, here's what we're going to do. We're going to build a LinkButton component. It's always going to look like a link, but it's going to be flexible in its implementation: it can either render an <a> tag, or a <button> tag, depending on whether an href is supplied.
Spend a few minutes tinkering, and then watch the video below to see how I'd approach this problem.
Acceptance Criteria:
- The
LinkButtoncomponent has an optional prop,href. - If an
hrefis provided,LinkButtonshould render an<a>tag. Otherwise, it should render a<button>tag.
Code Playground
Let's explore:
Video Summary
So there's a few ways we could solve this problem.
We could create a separate "branch" based on the href:
function LinkButton({ href, children,}) { // Branch 1: anchor if (href) { return ( <a href={href} className={styles.button} > {children} </a> ); }
// Branch 2: button return ( <button className={styles.button}> {children} </button> );}This works, but it's a bit of a bummer. Whenever I make a change to this component, I have to remember to edit both branches. For example, adding a ...delegated object:
function LinkButton({ href, children, ...delegated}) { // Branch 1: anchor if (href) { return ( <a href={href} className={styles.button} {...delegated} > {children} </a> ); }
// Branch 2: button return ( <button className={styles.button} {...delegated} > {children} </button> );}Instead, I prefer to solve this problem using a technique known as polymorphism.
The first time you see this approach, it looks a little funky, but fear not! I'll explain everything.
First, here's what it looks like:
function LinkButton({ href, children, ...delegated}) { const Tag = typeof href === 'string' ? 'a' : 'button';
return ( <Tag href={href} className={styles.button} {...delegated} > {children} </Tag> );}To understand what's going on here, it's helpful to look at the plain JS one, without the JSX making it harder to understand:
// This code:React.createElement( Tag, { href, className: styles.button, ...delegated }, children);
// Is the same as this code:<Tag href={href} className={styles.button} {...delegated}> {children}</Tag>The Tag variable will resolve either to the string "a" or "button". The type is dynamic, but either way, it's a string.
It's confusing because we generally reserve uppercase variable names for components like App or Slider. So it's weird that an uppercase variable is rendering a "native" DOM node.
Why does Tag have to be uppercase? Wouldn't it be more natural to make our variable "tag" instead?
Well, if we try that, we'll render literal <tag> HTML elements. 😬
<tag href="/add-transaction" class="button"> Add Transaction</tag>As we learned in Module 1, lowercase JSX tags are treated verbatim, while capitalized JSX tags are treated dynamically:
<Button/> // React.createElement(Button)<button/> // React.createElement('button')We need to use a capital letter so that the JSX compiler creates an element using the Tag variable, and not the "tag" string.
I know that this pattern is a bit of a headscratcher the first time you see it, but I think it's a pretty elegant solution to a tough problem.
Here's the final code from the video: